import { MikroORM } from '@mikro-orm/core';

// 备份代码: EntityGenerator 
//const what = meta.className.match(/([A-Z][a-z]+)+?/g).pop().toLowerCase();

import 'knex';
import { Knex, knex } from 'knex';

declare module "knex" {
    namespace Knex {
        interface QueryBuilder {
          paginate(no,size): Knex.QueryBuilder
          map(fn:(entity:any)=>any) : Knex.QueryBuilder
          filter(fn:(entity:any)=>any) : Knex.QueryBuilder
          withEntity(entityName : any,fieldName:string, fn ?: (qb : Knex.QueryBuilder) => void,as?:string,join?:((obj,datas)=>[]) |'asc'|'desc',ordeBy?): Knex.QueryBuilder
          mountORM(orm : MikroORM) : Knex.QueryBuilder
          fromORM(entityName,orm : MikroORM,as?:string) : Knex.QueryBuilder
          cloneORM(_entityName?) : Knex.QueryBuilder
          queryBuilderFromORM(entityName,as?:string) : Knex.QueryBuilder
          onlySelect(...args) : Knex.QueryBuilder
          populate(applySoftDelete?:boolean) : Promise<any[]>
          take(n:number,applySoftDelete?:boolean) : Promise<any[]>
          getOne(applySoftDelete?:boolean) : Promise<any>
          subQuerySql(as?:string) : any
          subQueryRaw(as?:string) : any
          findOrCreate(select,where,insert) : any
          existOrCreate(select,where,insert) : any
          updateOrCreate(select,where,insert) : any
          createOrUpdate(select,where,insert) : any
          selectAll<T>(excludes?: (keyof T)[]) : Knex.QueryBuilder
          selectExcludes<T>(entityName,excludes: (keyof T)[]) : Knex.QueryBuilder
          selectIncludes<T>(entityName,excludes: (keyof T)[]) : Knex.QueryBuilder
          applySoftDelete() : Knex.QueryBuilder
          softDelete() : Knex.QueryBuilder
          restore() : Knex.QueryBuilder 
        }
      }
  }


  knex.QueryBuilder.extend('findOrCreate',async function(selectParams,whereParams,insertParams) : Promise<any> {
    const knex = this['_knex']
    const entityName = this['_entityName']

    return await knex.transaction(trx => {
      this.cloneORM(entityName)
      .transacting(trx)
      .select(selectParams)
      .where(whereParams)
      .then(res => {
        if (res.length === 0) {
          return this.cloneORM(entityName).insert(insertParams).then(() => {
            return this.cloneORM(entityName).selectAll().where(insertParams);
          });
        } else {
          return res;
        }
      })
      .then(trx.commit)
      .catch(trx.rollback);
    })
  })

  knex.QueryBuilder.extend('createOrUpdate',async function(selectParams,whereParams,insertParams) : Promise<any> {
    const knex = this['_knex']
    const entityName = knex.tableName(this['_entityName']) 

    return await knex.transaction(trx => {
      knex(entityName)
      .transacting(trx)
      .select(selectParams)
      .where(whereParams)
      .then(res => {
        if (res.length === 0) {
          return knex(entityName).insert(insertParams).then(() => {
            return knex(entityName).where(insertParams);
          });
        } else {
          return knex(entityName).update(insertParams).where(whereParams).then(() => {
            return knex(entityName).where(whereParams);
          });
          return res;
        }
      })
      .then(trx.commit)
      .catch(trx.rollback);
    })
  })

  knex.QueryBuilder.extend('selectAll',function(excludes){
    const knex = this['_knex']
    this.select(knex.fieldsExcludes(this['_entityName'],excludes))
    return this
  })

  knex.QueryBuilder.extend('selectExcludes',function(entityName,excludes):any {
    const knex = this['_knex']
    this.select(knex.fieldsExcludes(entityName,excludes))
    return this
  })

  knex.QueryBuilder.extend('queryBuilderFromORM',function(entityName,as?:string):any {
    const knex = this['_knex']
    let qb = knex.fromORM(entityName,as)
    return qb
  })

  knex.QueryBuilder.extend('cloneORM',function(_entityName?):any {
    let qb = this.clone()
    qb.mountORM(this['_orm'])
    qb['_knex'] = this['_knex']
    qb['_entityName'] = _entityName || this['_entityName']
    return qb
  })

  knex.QueryBuilder.extend('subQuerySql',function(as:string):any {
    const sql = this.toQuery()
    as = as?as:  this['_orm'].getMetadata(this['_entityName']).collection
    return `(${sql}) as ${as}`
  })
  
  knex.QueryBuilder.extend('subQueryRaw',function(as:string):any {
    return this['_knex'].raw(this.subQuerySql())
  })
  
  knex.QueryBuilder.extend('fromORM',function(entityName,orm : MikroORM,as?:string){
    if(orm) this.mountORM(orm) 
    this['_entityName'] = entityName
    const meta = orm.getMetadata(entityName)
    this.from(as?meta.collection+' AS ' + as : meta.collection)
    this['_orm_entityName'] = entityName
    return this
  })
  
  knex.QueryBuilder.extend('onlySelect',function (...args){
    this.clearSelect()
    this.select(...args)
    return this  
  })
  
  knex.QueryBuilder.extend('mountORM',function(orm : MikroORM){
    this['_orm'] = orm;
    return this
  })
  
  knex.QueryBuilder.extend('paginate',function(no,size){
    this.offset(Math.max((no - 1),0) * size)
    this.limit(size);  
    return this
  })
  
  knex.QueryBuilder.extend('take',function(n:number,applySoftDelete=true) :any {
    this.limit(n);
    return this.populate(applySoftDelete)
  })
  
  knex.QueryBuilder.extend('getOne',async function(){
    this.limit(1)
    const result = await this.populate() 
    if(result && result.length > 0) return result[0] 
    return null
  }) 

  knex.QueryBuilder.extend('map',function(fn){
    this['_maps'] = this['_maps'] ? this['_maps'] : []
    const maps = this['_maps']
    maps.push(fn)
    return this
  }) 
  
  knex.QueryBuilder.extend('filter',function(fn){
    this['_filters'] = this['_filters'] ? this['_filters'] : []
    const filters = this['_filters']
    filters.push(fn) 
    return this
  })

  knex.QueryBuilder.extend('softDelete',function(){
    const knex = this['_knex']
    let params : any = { 'removed_at': knex.raw('NOW()')}
    return this.update(params)
  })

  knex.QueryBuilder.extend('restore',function(){
    const knex = this['_knex']
    let params : any = { 'removed_at': null}
    return this.update(params)
  })

  knex.QueryBuilder.extend('applySoftDelete',function(){
    const knex = this['_knex']
    return this.whereNull('removed_at')
  })

  knex.QueryBuilder.extend('populate', function(applySoftDelete=true) {
      //console.log(this.toQuery())

     if(applySoftDelete)
     {
        const meta = this['_orm'].getMetadata(this['_entityName'])

        if(meta && meta.comparableProps.findIndex((prop=>prop.name == 'removedAt')) >= 0)
        {
            this.applySoftDelete()
        }
     }

      return new Promise(async (resolve,inject)=>{
         try {
            await this.then(async(datas)=>{
              const _withs = this['_withs']

              let _with = null

              if(_withs && _withs.length > 0)
              {
                try {
                  while(_with = _withs.shift())
                  {
                      await _with.apply(this,[datas]);
                  }
                } catch (error) {
                    inject(error)
                }
              }
  
              const filters = this['_filters']
              
              if(filters)
              {
                filters.forEach(filter => {
                  datas = datas.filter(filter)
                });
              }

              const maps = this['_maps']

              if(maps)
              {
                maps.forEach(map => {
                  datas = datas.map(map)
                });
              }
    
              resolve(datas)
            })
          } catch (error) {
            //throw error
            // resolve(null)

            inject(error)
          }
      })
  })
  
  knex.QueryBuilder.extend('withEntity',function(entityName,fieldName,fn?,as?,join?,ordeBy?){
      this['_withs'] = this['_withs'] ? this['_withs'] : []
      const _withs = this['_withs']
  
      const orm = this['_orm'] as MikroORM;
      //const qb = this.queryBuilder() as Knex.QueryBuilder;
      const orgEntityName = this['_orm_entityName']
  
      const orgEntityMeta = orm.getMetadata(orgEntityName)
      const distEntityMeta = orm.getMetadata(entityName)
  
      const ref = orgEntityMeta.relations
      .find(relation => {
          let eq1 = relation.targetMeta.className === distEntityMeta.className
          let eq2 = relation.targetMeta.collection === distEntityMeta.collection
          let eq3 = relation.name == fieldName
          return eq1 && eq2 && eq3 
        } 
      )
      
      let inveRef = null
      let foreignKey : string = null;
      let primaryKey : string = null;
  
    if(ref.reference == "1:m" || ref.reference == "1:1") {
        inveRef = distEntityMeta.relations
        .find(relation => 
          relation.targetMeta === orgEntityMeta && 
          relation.inversedBy == fieldName
        )
        foreignKey = inveRef.joinColumns[0]
        primaryKey = ref.referencedPKs[0]
    }else if(ref.reference == "m:1")
    {
        foreignKey = ref.targetMeta.primaryKeys[0]
        primaryKey = ref.joinColumns[0]
    }else if (ref.reference == "m:n"){
        _withs.push(async function(datas) {
          let ids = datas.reduce((c,o)=>{
            let id = o[ref.referencedPKs[0]]
            if(id != undefined) c.push(id)
            return c
          },[])

          let query = this.queryBuilderFromORM(ref.pivotEntity,orm)

          let pivots = await query.whereIn(ref.joinColumns[0],ids).then()
     
          foreignKey = ref.targetMeta.primaryKeys[0]
          primaryKey = ref.inverseJoinColumns[0]

          ids = pivots.map(o=>o[primaryKey])
    
          let targetQuery = this.queryBuilderFromORM(entityName,orm).whereIn(foreignKey,ids)

          if(fn) fn(targetQuery); 

          let targets = await targetQuery.populate(false)

          datas.forEach((data : any) => {
              const matchPivots = pivots.filter((pivot)=>pivot[ref.joinColumns[0]] == data[ref.referencedPKs[0]])

              let matchs = matchPivots.map((pivot)=>{
                  var target = targets.find((target)=> target[ref.referencedColumnNames[0]] == pivot[ref.inverseJoinColumns[0]])
                  if(join && target){
                    return {pivot,target}
                  } 
                  else {
                    return target
                  }  
              })

              if(join && typeof(join) == 'function')
              {
                data[as?as:ref.name] = join(data[as?as:ref.name],matchs)
              }
              if(join && typeof(join) == 'string')
              {
                 ordeBy = ordeBy || 'id'

                 if(join == 'desc'){
                    data[as?as:ref.name] = matchs.sort((a,b)=> b.pivot[ordeBy] - a.pivot[ordeBy]).map(e=>e.target)
                 }else {
                    data[as?as:ref.name] = matchs.sort((a,b)=> a.pivot[ordeBy] - b.pivot[ordeBy]).map(e=>e.target)
                 }
              }
              else{
                  data[as?as:ref.name] = matchs
              }
          })
          
          return datas
        })

        return this
    }

    this.select(primaryKey)
  
    _withs.push(async function(datas) //, obj, builder
    {     
        const ids = datas.reduce((c,o)=>{
          let id = o[primaryKey]
          if(id != undefined) c.push(id)
          return c
        },[])
  
        const query = this.queryBuilderFromORM(entityName,orm)
      
        query.select('*')
  
        if(fn) fn(query);
  
        query.select(foreignKey)
  
        let results = await query.andWhere(foreignKey,'IN',ids)
          .populate(false)
          
        if(ref.reference == "1:m")
        {
            datas.forEach((data : any) => {
              const matchs = results.filter(filler=>filler[foreignKey] == data[primaryKey])
              data[as?as:ref.name] = matchs
            })
        }else if(ref.reference == "m:1") {
          datas.forEach((data : any) => {
            const match = results.find(filler=>filler[foreignKey] == data[primaryKey])
            data[as?as:ref.name] = match
          })
        }
        else if(ref.reference == "1:1") {
            datas.forEach((data : any) => {
              const match = results.find(filler=>filler[foreignKey] == data[primaryKey])
              data[as?as:ref.name] = match
            })
        }
       
        return datas;
    })
  
    return this
  })